Profile picture

[Java] jsoup을 사용한 자바 크롤링 구현

Amaranth2023년 07월 12일

개요


jsoup 라이브러리를 사용해서 웹사이트를 크롤링해보자.

예제로 내 블로그의 Java 카테고리 내 게시글들을 크롤링해보았다.

jsoup


자바로 만들어진 HTML 파서(Parser).

jsoup 라이브러리는 다음의 기능들을 제공한다.

  • URL, 파일, 문자열을 소스로 하여 HTML 문서를 파싱할 수 있다.
  • 문서 내의 HTML 요소, 속성, 텍스트를 조작할 수 있다.
  • CSS 선택자를 사용해 데이터를 추출할 수 있다.

요약하면 HTML 문서를 조작할 수 있게 해주는 라이브러리로, 해당 라이브러리를 통해 웹사이트로부터 원하는 데이터를 추출할 수 있다.

  • 의존성 추가
    implementation 'org.jsoup:jsoup:1.13.1'

document 불러오기

먼저 크롤링하고자 하는 웹사이트의 url을 가져온다.

https://amaran-th.github.io/

Untitled

만약 특정 카테고리의 게시글들만 크롤링할 경우, 다음과 같이 카테고리명이 url에 포함될 것이다.

‘Java’ 카테고리를 크롤링 해보자.

https://amaran-th.github.io/Java/

Untitled

url의 Document 문서를 불러오는 코드는 다음과 같다.

final String url = "https://amaran-th.github.io/Java/";

Connection connection = Jsoup.connect(url);
Document document = connection.get();

HTML 요소 추출하기

개발자도구의 Elements를 확인해보면, 각 게시글 블럭은 다음과 같은 하나의 <li> 태그로 구성되어 있다.

<li
  class="flex justify-between justify-items-center border-2 rounded-md shadow-md p-4 my-4 hover:bg-slate-100"
>
  <article class="" itemscope="" itemtype="http://schema.org/Article">
    <header class="mb-4">
      <h2 class="mt-2 text-main text-3xl font-title">
        <span itemprop="headline">[Java] jgrapht 사용법</span>
      </h2>
      <span class="text-sub text-sm"
        >May 21, 2023<span class="font-bold mx-2 text-gray-400 text-sm"
          >Java</span
        ></span
      >
    </header>
    <section>
      <p class="mb-0" itemprop="description">
        Java 라이브러리 jgrapht의 사용법에 대해 알아보자.
      </p>
    </section>
  </article>
  <div class="min-w-[5rem] max-w-[20rem] w-[150px] h-[150px]">
    <div
      class="rounded-xl h-full w-full gatsby-image-wrapper"
      style="position: relative; overflow: hidden"
    >
      <div aria-hidden="true" style="width: 100%; padding-bottom: 78.5%"></div>
      <img
        aria-hidden="true"
        src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAAAsTAAALEwEAmpwYAAABiklEQVQ4y42Th8rCQBCE7/1fyQcQIWDDFjsW7Nh73J9vYeUS80cXjlzbudmZjZN/4vV6vb+bzUbW67VEURQ7SwuXBUZ0Oh0ZjUYynU6l1Wp9Bc0EhFmv15Pj8SiDwUDG47Esl8vvgBz6JdqcMmEHs8lkIvP5PAaYlueSevlBeYVCQWazmYLn83m5XC6ZErnk5uPxkPv9rnOA+v2+slosFspyOBzqGXeu16ve9/M/SiYBANjV6/UPNt1uVx8qlUqSy+Wk0Wikl2yAgPEq7u73+3fp5i7MarWaHA4HdX6328UZ+mAWgFKqgfmamhQ4nqalM7DVaqUgaNVsNjXZd9KGgYZhqAxpJ9yPmWIHlIo2JnxW4HoQBPJ8PvW+VaSA9Nl2u5VqtaqvITRlMVgn50hSLpfldDpJu93WASFaSgHZQJNisagvVyoVLd0H8tcA8jhzOoHmx0QMc6YfG1xEP3PXgjXJftCD3CWHVkK2t8sE/ysszufzR6sYu+T+7XZTQL91XNov569/mft9+AcJYNOtrRLl/QAAAABJRU5ErkJggg=="
        alt=""
        style="
          position: absolute;
          top: 0px;
          left: 0px;
          width: 100%;
          height: 100%;
          object-fit: cover;
          object-position: center center;
          opacity: 0;
          transition-delay: 500ms;
        "
      /><picture
        ><source
          srcset="
            /static/2f2c5ac60692d07dc14284436917fdf8/69585/graph.png 200w,
            /static/2f2c5ac60692d07dc14284436917fdf8/497c6/graph.png 400w,
            /static/2f2c5ac60692d07dc14284436917fdf8/c30fe/graph.png 433w
          "
          sizes="(max-width: 433px) 100vw, 433px" />
        <img
          sizes="(max-width: 433px) 100vw, 433px"
          srcset="
            /static/2f2c5ac60692d07dc14284436917fdf8/69585/graph.png 200w,
            /static/2f2c5ac60692d07dc14284436917fdf8/497c6/graph.png 400w,
            /static/2f2c5ac60692d07dc14284436917fdf8/c30fe/graph.png 433w
          "
          src="/static/2f2c5ac60692d07dc14284436917fdf8/c30fe/graph.png"
          alt=""
          loading="lazy"
          style="
            position: absolute;
            top: 0px;
            left: 0px;
            width: 100%;
            height: 100%;
            object-fit: cover;
            object-position: center center;
            opacity: 1;
            transition: opacity 500ms ease 0s;
          " /></picture
      ><noscript
        ><picture
          ><source
            srcset="
              /static/2f2c5ac60692d07dc14284436917fdf8/69585/graph.png 200w,
              /static/2f2c5ac60692d07dc14284436917fdf8/497c6/graph.png 400w,
              /static/2f2c5ac60692d07dc14284436917fdf8/c30fe/graph.png 433w
            "
            sizes="(max-width: 433px) 100vw, 433px" />
          <img
            loading="lazy"
            sizes="(max-width: 433px) 100vw, 433px"
            srcset="
              /static/2f2c5ac60692d07dc14284436917fdf8/69585/graph.png 200w,
              /static/2f2c5ac60692d07dc14284436917fdf8/497c6/graph.png 400w,
              /static/2f2c5ac60692d07dc14284436917fdf8/c30fe/graph.png 433w
            "
            src="/static/2f2c5ac60692d07dc14284436917fdf8/c30fe/graph.png"
            alt=""
            style="
              position: absolute;
              top: 0;
              left: 0;
              opacity: 1;
              width: 100%;
              height: 100%;
              object-fit: cover;
              object-position: center;
            " /></picture
      ></noscript>
    </div>
  </div>
</li>

Untitled

여기서, 게시글의 제목, 업로드 날짜, 요약 설명, 섬네일 이미지를 불러오고 싶다고 하자.

해당 정보가 포함된 태그만 추출해보면 다음과 같을 것이다.

<span itemprop="headline">[OS] 프로세스와 스레드</span>
<span class="text-sub text-sm">June 06, 2023<span class="font-bold mx-2 text-gray-400 text-sm">Java</span></span>
<p class="mb-0" itemprop="description">프로세스와 스레드의 차이를 알아보자</p>
<img sizes="(max-width: 800px) 100vw, 800px" srcset="/static/71f2accb4cd3c9809a2be633d413a9da/69585/process.png 200w,
/static/71f2accb4cd3c9809a2be633d413a9da/497c6/process.png 400w,
/static/71f2accb4cd3c9809a2be633d413a9da/ee604/process.png 800w,
/static/71f2accb4cd3c9809a2be633d413a9da/f3583/process.png 1200w,
/static/71f2accb4cd3c9809a2be633d413a9da/5707d/process.png 1600w,
/static/71f2accb4cd3c9809a2be633d413a9da/4f3db/process.png 2991w" src="/static/71f2accb4cd3c9809a2be633d413a9da/ee604/process.png" alt="" loading="lazy" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; object-fit: cover; object-position: center center; opacity: 1; transition: opacity 500ms ease 0s;">

이제 쿼리 선택자를 이용해 Document로부터 각 상품의 데이터를 추출해야 한다.

아래처럼 개발자 도구의 [Element]에서 특정하고자 하는 요소를 [Copy selector]하여 요소 선택자를 가져올 수 있다.

Untitled

이렇게 제목, 업로드 날짜, 요약 설명, 섬네일 이미지에 대한 선택자를 가져왔다.

String TITLE_SELECTOR = "body > div > div > div.w-full.flex.max-w-6xl.justify-between > div > ol > a:nth-child(1) > li > article > header > h2 > span";
String DATE_SELECTOR = "body > div > div > div.w-full.flex.max-w-6xl.justify-between > div > ol > a:nth-child(1) > li > article > header > span";
String DESCRIPTION_SELECTOR ="body > div > div > div.w-full.flex.max-w-6xl.justify-between > div > ol > a:nth-child(1) > li > article > section > p";
String IMAGE_SELECTOR = "body > div > div > div.w-full.flex.max-w-6xl.justify-between > div > ol > a:nth-child(1) > li > div > div > picture > img";

앞서 불러왔던 document에 대해 select(), selectFirst() 등을 통해 선택자 조건에 부합하는 요소(들)을 불러올 수 있다.

Document document = connection.get();

final Elements titleElements = document.select(TITLE_SELECTOR);
final Elements dateElements = document.select(DATE_SELECTOR);
final Elements descriptionElements = document.select(DESCRIPTION_SELECTOR);
final Elements imageElements = document.select(IMAGE_SELECTOR);

(왠진 모르겠지만 개발자도구에서 추출한 Selector가 안 먹혀서 직접 다시 만들어주었다. 아마 tailwindcss 때문에 클래스가 복잡해져서 선택자 추출이 잘 안된 듯 하다.)

예시로 불러온 title 태그들을 콘솔에 출력해보면 다음과 같은 결과를 확인할 수 있다.

final Elements titleElements = document.select(TITLE_SELECTOR);

System.out.println(titleElements);

Untitled

이렇게 위에서 보여준 4개의 태그를 불러올 수 있다.(페이지 내 모든 게시글에 대해 불러와짐.)

요소로부터 데이터 추출하기

제목, 날짜, 요약 설명의 경우 text 요소로 정의되어 있기 때문에 Element.text() 메서드로 불러오고 파싱한다.

<span itemprop="headline">[OS] 프로세스와 스레드</span>
final List<String> titles = titleElements.stream()
    .map(it -> it.text())
    .collect(Collectors.toList());

System.out.println(titles);

Untitled

이 중에서 날짜는 다음과 같이 카테고리와 한 태그에 같이 묶여있는데, 여기서 두 데이터를 모두 감싸고 있는 <span>태그의 text를 추출하면 “Jun 06, 2023Java”와 같이 파싱이 된다.

그래서 String 메서드를 사용해 날짜만 추출하도록 처리하였다.

<span class="text-sub text-sm">June 06, 2023<span class="font-bold mx-2 text-gray-400 text-sm">Java</span></span>
final List<String> dates = dateElements.stream()
    .map(it ->{
      final String[] split = it.text().split(", ");
      return String.join(", ",split[0],split[1].substring(0,4));
    })
    .collect(Collectors.toList());

Untitled

그리고 이미지 url의 경우 다음과 같이 태그의 src 속성 값으로 저장되어 있다.

<img sizes="(max-width: 800px) 100vw, 800px" srcset="/static/71f2accb4cd3c9809a2be633d413a9da/69585/process.png 200w,
/static/71f2accb4cd3c9809a2be633d413a9da/497c6/process.png 400w,
/static/71f2accb4cd3c9809a2be633d413a9da/ee604/process.png 800w,
/static/71f2accb4cd3c9809a2be633d413a9da/f3583/process.png 1200w,
/static/71f2accb4cd3c9809a2be633d413a9da/5707d/process.png 1600w,
/static/71f2accb4cd3c9809a2be633d413a9da/4f3db/process.png 2991w" src="/static/71f2accb4cd3c9809a2be633d413a9da/ee604/process.png" alt="" loading="lazy" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; object-fit: cover; object-position: center center; opacity: 1; transition: opacity 500ms ease 0s;">

Element.attr() 메서드로 추출한다.

final List<String> images = imageElements.stream()
    .map(it -> PREFIX_URL+it.attr("src"))
    .collect(Collectors.toList());

System.out.println(images);

Untitled

추출 코드 전문은 다음과 같다.

final Elements titleElements = document.select(TITLE_SELECTOR);
final Elements dateElements = document.select(DATE_SELECTOR);
final Elements descriptionElements = document.select(DESCRIPTION_SELECTOR);
final Elements imageElements = document.select(IMAGE_SELECTOR);

final List<String> titles = titleElements.stream()
    .map(it -> it.text())
    .collect(Collectors.toList());

final List<String> dates = dateElements.stream()
    .map(it ->{
      final String[] split = it.text().split(", ");
      return String.join(", ",split[0],split[1].substring(0,4));
    })
    .collect(Collectors.toList());

final List<String> descriptions = descriptionElements.stream()
    .map(it -> it.text())
    .collect(Collectors.toList());

final List<String> images = imageElements.stream()
    .map(it -> PREFIX_URL+it.attr("src"))
    .collect(Collectors.toList());

마무리


이렇게 jsoup 라이브러리를 사용해서 특정 웹사이트로부터 원하는 데이터를 추출하는 방법을 알아봤다.

나중에 기회가 되면, 여러 페이지를 순차적으로 크롤링하는 코드를 짜보고 일정 시간마다 크롤러가 실행되도록 셋팅하는 작업을 포스팅해보도록 하겠다.

참고 자료


jsoup : 자바 HTML 파서(Java HTML Parser)

[Java] - Jsoup을 이용한 크롤링(feat. 인프런)


Loading script...